package com.baidu.bas.security.interceptor;

import cn.hutool.json.JSONUtil;
import com.baidu.bas.core.annotation.EruptRecordOperate;
import com.baidu.bas.core.annotation.EruptRouter;
import com.baidu.bas.core.service.EruptCoreService;
import com.baidu.bas.core.view.EruptFieldModel;
import com.baidu.bas.core.view.EruptModel;
import com.baidu.bas.security.config.EruptSecurityProp;
import com.baidu.bas.security.func.OperationLogInterceptor;
import com.baidu.bas.security.service.SecurityService;
import com.baidu.bas.upms.constant.EruptReqHeaderConst;
import com.baidu.bas.upms.model.EruptOrg;
import com.baidu.bas.upms.model.EruptUser;
import com.baidu.bas.upms.model.log.EruptOperateLog;
import com.baidu.bas.upms.service.IUserService;
import com.baidu.bas.upms.util.IpUtil;
import com.baidu.bas.upms.util.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import static com.baidu.bas.annotation.constant.ReqAttrKey.TENANCY;
import static com.baidu.bas.annotation.constant.ReqAttrKey.TENANCY_HEADER;

/**
 * @author liyuepeng
 * @date 2019-12-05.
 */
@Service
@Slf4j
public class LoginInterceptor extends HandlerInterceptorAdapter {

    private static final String REQ_DATE = "@req_date@";

    static final String REQ_BODY = "@req_body@";

    @Resource
    private IUserService iUserService;

    @Resource
    private SecurityService securityService;

    private static final String ERUPT_PARENT_HEADER_KEY = "eruptParent";

    private static final String ERUPT_PARENT_PARAM_KEY = "_eruptParent";

    @Autowired
    private EruptSecurityProp eruptSecurityProp;

    @Autowired
    private EntityManager entityManager;

    @Autowired(required = false)
    private final List<OperationLogInterceptor> operationLogProxies = Collections.emptyList();

    private boolean isWhitelisted(HttpServletRequest request) {
        if(eruptSecurityProp.getWhiteList()==null){
            return false;
        }
        val path = request.getRequestURI();
        if(path.startsWith("/actuator/")){
            return true;
        }
        if(path.startsWith("/magic/web")){
            return true;
        }
        log.debug("{} check in [{}]",path, Arrays.asList(eruptSecurityProp.getWhiteList()));
        for (String pathRoot : eruptSecurityProp.getWhiteList()) {
            if (path.startsWith(pathRoot)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        request.setAttribute(REQ_DATE, System.currentTimeMillis());

        //注入租户
        if(!RequestMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())
                && !isWhitelisted(request)){
            injectTenancy(request);
        }

        EruptRouter eruptRouter = null;
        if (handler instanceof HandlerMethod) {
            eruptRouter = ((HandlerMethod) handler).getMethodAnnotation(EruptRouter.class);
        }
        if (null == eruptRouter) {
            return true;
        }

        String path = request.getServletPath();
        String resource = path.split("/")[eruptRouter.skipAuthIndex() + eruptRouter.authIndex()];

        String token = null;
        String eruptName = null;
        String parentEruptName = null;
        if (eruptRouter.verifyMethod() == EruptRouter.VerifyMethod.HEADER) {
            token = RequestUtils.getSecretHeaderValue(request);
            eruptName = request.getHeader(EruptReqHeaderConst.ERUPT_HEADER_KEY);
            parentEruptName = request.getHeader(ERUPT_PARENT_HEADER_KEY);
        } else if (eruptRouter.verifyMethod() == EruptRouter.VerifyMethod.PARAM) {
            token = request.getParameter(EruptReqHeaderConst.URL_ERUPT_PARAM_TOKEN);
            eruptName = request.getParameter(EruptReqHeaderConst.URL_ERUPT_PARAM_KEY);
            parentEruptName = request.getHeader(ERUPT_PARENT_PARAM_KEY);
        }
        //默认erupt name 和 resource 相同
        if(eruptName==null){
            eruptName = resource;
        }
        log.debug("[authoring]  resource: {}, erupt name: {}", resource,eruptName);

        if (eruptRouter.verifyType().equals(EruptRouter.VerifyType.ERUPT)) {
            EruptModel erupt = EruptCoreService.getErupt(eruptName);
            if (null == erupt) {
                response.setStatus(HttpStatus.NOT_FOUND.value());
                return false;
            }
            if (!erupt.getErupt().authVerify()) {
                return true;
            }
        }
        // TODO 在这里验证token
        if (null == token || !securityService.verifyToken(token)) {
            log.debug("[Unauthorized]  {}",token);
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.sendError(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        //权限校验
        switch (eruptRouter.verifyType()) {
            case LOGIN:
                break;
            case MENU:
                //菜单 从url中截取路径
                if (!resource.equals(eruptName)) {
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.sendError(HttpStatus.FORBIDDEN.value());
                    return false;
                }
                if (!securityService.verifyMenuAuth(token, eruptName)) {
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.sendError(HttpStatus.FORBIDDEN.value());
                    return false;
                }
                break;
            case ERUPT:
                EruptModel eruptModel = EruptCoreService.getErupt(eruptName);
                if (null == eruptModel) {
                    response.setStatus(HttpStatus.NOT_FOUND.value());
                    return false;
                }
                //eruptParent logic
                $ep:
                if (StringUtils.isNotBlank(parentEruptName)) {
                    EruptModel eruptParentModel = EruptCoreService.getErupt(parentEruptName);
                    for (EruptFieldModel model : eruptParentModel.getEruptFieldModels()) {
                        if (eruptModel.getEruptName().equals(model.getFieldReturnName())) {
                            if (resource.equals(eruptModel.getEruptName())) {
                                resource = eruptParentModel.getEruptName();
                            }
                            eruptModel = eruptParentModel;
                            break $ep;
                        }
                    }
                    response.setStatus(HttpStatus.NOT_FOUND.value());
                    return false;
                }
                if (!path.contains(eruptName) || !securityService.verifyEruptMenuAuth(token, resource, eruptModel)) {
                    log.warn("[Unauthorized]  path: {} , erupt: {}, token: {}, resource: {}, model: {}",path,eruptName,token,resource,eruptModel);
                    response.setStatus(HttpStatus.FORBIDDEN.value());
                    response.sendError(HttpStatus.FORBIDDEN.value());
                    return false;
                }
                break;
            default:
                break;
        }
        return true;
    }

    private void injectTenancy(HttpServletRequest request) {
        String tenancy = request.getHeader(TENANCY_HEADER);
        if(StringUtils.isNotBlank(tenancy)){
            request.setAttribute(TENANCY, tenancy);
            return;
        }
        EruptUser currentEruptUser = iUserService.getCurrentEruptUser();
        log.debug("erupt user: {}",currentEruptUser);
        if(currentEruptUser!=null){
            EruptOrg eruptOrg = currentEruptUser.getEruptOrg();
            log.debug("erupt org: {}",eruptOrg);
            if(eruptOrg!=null){
                request.setAttribute(TENANCY, eruptOrg.getCode());
            }
        }
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    @Transactional(rollbackOn = Exception.class)
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (eruptSecurityProp.isRecordOperateLog()) {
            if (handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                EruptRecordOperate eruptOperate = handlerMethod.getMethodAnnotation(EruptRecordOperate.class);
                if (null != eruptOperate) {
                    EruptRouter eruptRouter = handlerMethod.getMethodAnnotation(EruptRouter.class);
                    EruptOperateLog operate = new EruptOperateLog();
                    if (null != eruptRouter && eruptRouter.verifyType() == EruptRouter.VerifyType.ERUPT) {
                        String eruptName;
                        if (eruptRouter.verifyMethod() == EruptRouter.VerifyMethod.HEADER) {
                            eruptName = request.getHeader(EruptReqHeaderConst.ERUPT_HEADER_KEY);
                        } else {
                            eruptName = request.getParameter(EruptReqHeaderConst.URL_ERUPT_PARAM_KEY);
                        }
                        //默认erupt
                        String path = request.getServletPath();
                        String resource = path.split("/")[eruptRouter.skipAuthIndex() + eruptRouter.authIndex()];
                        if(eruptName==null){
                            eruptName = resource;
                        }
                        operate.setApiName(eruptOperate.desc() + " | " + EruptCoreService.getErupt(eruptName).getErupt().name());
                    } else {
                        operate.setApiName(eruptOperate.desc());
                    }
                    Object param = request.getAttribute(REQ_BODY);
                    if (null != param) {
                        operate.setReqParam(param.toString());
                    }
                    operate.setIp(IpUtil.getIpAddr(request));
                    operate.setRegion(IpUtil.getCityInfo(operate.getIp()));
                    operate.setStatus(true);
                    operate.setReqMethod(request.getMethod());
                    //url中包含完整地址
                    operate.setReqAddr(request.getRequestURL().toString());
                    if(!isWhitelisted(request)){
                        operate.setEruptUser(new EruptUser() {{
                            this.setId(iUserService.getCurrentUid());
                        }});
                    }
                    Date date = new Date();
                    operate.setCreateTime(date);
                    operate.setTotalTime(date.getTime() - (Long) request.getAttribute(REQ_DATE));
                    operate.setReqHeader(JSONUtil.toJsonStr(RequestUtils.getHeaders(request)));
                    if (null != ex) {
                        operate.setErrorInfo(ExceptionUtils.getStackTrace(ex));
                        operate.setStatus(false);
                    }
                    operationLogProxies.forEach(it->{
                        it.record(operate,request,response);
                    });
                    entityManager.persist(operate);
                }
            }
        }
    }


    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        super.afterConcurrentHandlingStarted(request, response, handler);
    }
}
